Day 9 架了兩道花費邊界之後,有一個問題它們完全答不了:如果 Azure OpenAI 的 API key 外洩了呢?拿到 key 的人不會經過你的 FastAPI,他直接打 https://<resource>.openai.azure.com。你的 token budget、你的 max_output_tokens、你的 usage log,一項都輪不到出場。
今天用 Azure API Management(APIM)補上這一層:讓 model 憑證從所有應用程式裡消失,把「誰能花錢」變成 gateway 上可撤銷、可限流的 policy。讀完你會有一個實測過的 Consumption tier 配置,和一張「這個 tier 買不到什麼」的誠實清單。
Day 9 的 guardrail 是應用程式邏輯:它們活在 request handler 裡,管的是「經過這個 backend 的流量」。這個前提平常隱形,出事時致命——憑證外洩的本質就是流量不再經過你。
而 key 這種東西,洩漏的路徑多到數不完:commit 進 repo、貼進 issue、躺在 CI log、進了前端 bundle。只要有一份長期有效的靜態憑證存在於應用程式設定裡,「不外洩」就是機率問題,不是工程保證。所以今天的目標不是把 key 藏得更好,而是讓應用程式不再保管或使用 model key。Azure OpenAI resource 仍保留 local/key authentication;要讓舊 key 本身失效,還得另外停用 local auth,本篇不做。
講 APIM 保護 GenAI API,有兩種常被混為一談的拓撲:
APIM 擋在 FastAPI 前面:保護「我們的 API」,也就是對外的認證卸載、per-client 限流 /chat。
APIM 擋在 model endpoint 前面:保護「模型」。我們的 backend(以及未來組織裡任何服務)改打 APIM,不再直連 Azure OpenAI。這是官方文件說的 AI gateway pattern(AI gateway 概觀,查核 2026-07)。
第一種是被我否決的方案,不是不對,是現在做沒有意義:我們的 API 目前沒有身份系統(Day 19 才有),gateway 在前面能驗的只有「有沒有 key」,而這件事對匿名 demo API 毫無增益。第二種會縮小憑證暴露面:做完之後,AZURE_OPENAI_API_KEY 可以從應用程式環境變數中移除。所以今天做第二種。
對 request body 與 Responses API schema 沒有侵入:gateway 把 https://<apim>.azure-api.net/genai/* 映到 https://<aoai>.openai.azure.com/openai/v1/*,Day 5 以來的 /responses payload 原樣通過。Backend 仍需更換 base URL,並加上 APIM subscription header。
這件事能成立不是理所當然。APIM 的 AI gateway 明列支援 Responses API schema,匯入精靈也有專門的「Azure OpenAI v1」相容選項(Foundry API 匯入文件,查核 2026-07)。這在 2026 年是新事,v1 GA endpoint(Day 4)在 gateway 這層是一等公民了。
收編憑證後,認證拆成兩段,各自用適合的機制:
上游(gateway → Azure OpenAI):APIM 用自己的 system-assigned managed identity 取 Entra token,policy 是 authentication-managed-identity(policy 文件,查核 2026-07)。
RBAC 這端只要一條:把 Cognitive Services OpenAI User 角色給 gateway 的身份,scope 是 Azure OpenAI 帳戶。
看清楚這個配置的性質:APIM 到 Azure OpenAI 的呼叫路徑沒有讀取或儲存 model key,而是使用 managed identity。這不等於 resource 已停用 key authentication;它解決的是這條應用路徑的 key custody。
下游(client → gateway):每個 client 一把 APIM subscription key(Ocp-Apim-Subscription-Key header)。它跟 model key 的差別不在強度,在治理性質:per-client 發放、獨立撤銷、獨立限流、用量可歸因到 subscription/消費端。沒有身份映射時,它不能可靠歸因到自然人。model key 是「一把鑰匙開金庫」,subscription key 則像每個消費端各拿一張可撤銷的門禁卡。
policy 的核心就這幾行(完整檔在 infra/apim/genai-api-policy.xml):
<inbound>
<base />
<rate-limit calls="5" renewal-period="60" />
<authentication-managed-identity resource="https://cognitiveservices.azure.com"
output-token-variable-name="msi-token" ignore-error="false" />
<set-header name="Authorization" exists-action="override">
<value>@("Bearer " + (string)context.Variables["msi-token"])</value>
</set-header>
<set-header name="api-key" exists-action="delete" />
</inbound>
最後那行 api-key delete 值得停一秒:就算哪個 client 手上還留著一把 model key 順手送上來,它也會在 gateway 被摘掉。client 的憑證到 gateway 為止,上游只認 gateway 自己的身份。這是「credential 邊界」的具體長相。
本系列的成本紀律把 APIM 釘死在 Consumption tier(per-call 計費、每月首 1M calls 免費;Developer tier 約 US$50/月,明確禁用;定價頁,查核 2026-07)。這個選擇有牙齒:AI gateway 最招牌的幾個 policy,在這個 tier 上不存在(各 policy 文件的 gateway 支援清單,查核 2026-07):
| 能力 | Policy | Consumption |
|---|---|---|
| per-subscription 呼叫數限流 | rate-limit |
✅ |
| per-subscription 呼叫數配額 | quota |
✅ |
| token 用量 metrics | llm-emit-token-metric |
✅(需接 Application Insights) |
| managed identity 後端認證 | authentication-managed-identity |
✅ |
| token 限流/token 配額 | llm-token-limit |
❌ |
| 任意 key 限流(IP、JWT claim…) | rate-limit-by-key |
❌ |
| semantic caching | llm-semantic-cache-lookup/-store |
❌(本來就需外接 Redis) |
所以挑 tier 就是在挑你的治理詞彙。在 Consumption 上,gateway 的限流單位只有 calls:一個 call 可以是 10 個 token 也可以是十萬個。想在 gateway 層說「這個 client 每分鐘最多 N 個 token」(llm-token-limit,TPM 超額回 429 帶 Retry-After 與 remaining-tokens header),得上 dedicated 或 v2 tier。
忍喵:「看到llm-token-limit不支援就想跳 tier 的人,先回答一個問題:token 維度現在是誰在管?如果 Day 9 那層還在,你想買的東西你已經有了,只是住在不同樓層。」
這對本系列不是災難,因為 token 維度本來就有人管:Day 9 的應用層 metering。分工反而更乾淨:gateway 管「誰、多頻繁」,應用層管「花多少 token」。但如果你的場景是把 gateway 當組織級 AI 閘道、背後掛十個消費方,token 限流就是核心需求,Consumption 不是你的 tier。這是本篇做法的「不適用情境」第一條。
另一個要人工對齊的細節:llm-token-limit/llm-emit-token-metric 是現行的 provider 中立命名,舊的 azure-openai-token-limit 系列是同能力的前身;查文件時認 llm-*。而「AI gateway」不是獨立產品或 SKU,就是 APIM 的能力集。
以下全部是 2026-07-22 在 japaneast 的實測(configure-apim.sh 建立、測完 delete-apim.sh 拆除;Consumption tier 建立只花幾分鐘,dedicated tier 要 30–45 分)。先看正常路徑,帶 subscription key 打 gateway 的 /responses:
curl -sS https://<apim>.azure-api.net/genai/responses \
-H "Ocp-Apim-Subscription-Key: $DEMO_KEY" -H 'Content-Type: application/json' \
-d '{"model": "chat-mini", "input": "Reply with exactly: pong"}'
回應 HTTP 200,output_text 是 pong,usage 完整通過(input_tokens: 11, output_tokens: 116, reasoning_tokens: 64)。Day 9 的應用層 metering 在 gateway 後面照常運作,兩層各管各的。
然後是這次實測最有價值的意外。照理說沒帶 key 該吃 401,但第一次測試時,keyless 的 curl 拿到了 HTTP 200:gateway 是一個對全世界開放的 model 代理。原因是一個危險的預設值:az apim api create 建出來的 API,subscriptionRequired 預設是 false。入口網站的匯入精靈會幫你勾上,但用 CLI/IaC 自建的人不主動宣告就是開的。
修法一個 flag(--subscription-required true,已進 script),但教訓值得放大:gateway 的存在不等於 gateway 在把關,部署完的第一個測試應該是「不帶憑證打打看」。修正後:
{ "statusCode": 401, "message": "Access denied due to missing subscription key. ..." }
忍喵:「架好 gateway、設好 managed identity、寫好 policy,然後 API 預設不驗 key——開放代理是『裝好了防盜門忘了上鎖』。驗收清單第一條永遠是:不帶憑證打一發。」
最後是 rate-limit。policy 設 calls="5"、60 秒窗,連打七發:
call 1: HTTP 200
...
call 5: HTTP 200
call 6: HTTP 429
call 7: HTTP 429
429 的回應本體與 header:
HTTP/1.1 429 Too Many Requests
Retry-After: 54
{ "statusCode": 429, "message": "Rate limit is exceeded. Try again in 54 seconds." }
注意兩件事:這個 429 帶 Retry-After,跟 Day 9 對話預算的 429(不回補、等待無用、刻意不帶 Retry-After)語意相反,時間窗會回補的限流才配給重試建議。以及它的錯誤形狀是 APIM 的 statusCode/message,不是我們的 error envelope。這條界線下面誠實揭露會再談。
{"error": {...}, "correlation_id"} 是兩套。現階段這條界線無妨(打 gateway 的是我們自己的 backend);等 Day 19 把 gateway 擺到對外 API 前面,「兩種錯誤形狀怎麼跟 client 講清楚」就是必須處理的合約問題。calls="5"、60 秒窗,是為了讓你在一個迴圈裡看到 429 選的數字;生產值得從觀測到的流量長出來。day-10 tag(repo,docs 里程碑):
docs/api-management.md:拓撲決策、Consumption tier 取捨表、分層 guardrail 全景infra/apim/genai-api-policy.xml:rate-limit + managed identity + 憑證剝除的完整 policyinfra/scripts/configure-apim.sh/delete-apim.sh:一鍵建立與拆除(含 RBAC 清理與 purge),全程沒有任何一步接觸 model key。拆除也踩了一個坑:刪 identity 時用 --assignee 查角色指派會因 graph 查不到人而漏刪,孤兒指派會留在 Azure OpenAI 帳戶上;script 已改用 principalId 過濾拉遠看,Day 9 結尾那張分層圖現在多了一層:
| 層 | 管什麼 | 單位 | 時機 |
|---|---|---|---|
App:max_output_tokens |
單次回覆 | tokens | 呼叫當下 |
| App:conversation budget | 單一對話 | tokens | inference 之前 |
Gateway:rate-limit |
單一 client(subscription) | calls | gateway,近似 |
| Deployment quota | 單一 deployment | TPM | 上游 |
| Budget alert | 訂閱 | 金額 | 延遲通知 |
每一層擋的都是其他層看不見的失控:app 看不見別的消費方,gateway(在這個 tier)看不見 token,budget alert 什麼都擋不了、只負責遲到地說一聲。
下一篇進 Part 3:RAG。模型再會限流、再會省錢,缺少專有資料時仍沒有可核對的回答依據。Day 11 從 backend 工程師的視角拆解 RAG 到底是什麼、不是什麼。
用到的 Azure 服務:Azure API Management(Consumption tier)、Azure OpenAI(gpt-5-mini via Responses API)、Microsoft Entra ID(managed identity + RBAC)。
本文由作者規劃與撰寫,AI(Claude)協助草稿整理與程式碼驗證;技術內容與觀點由作者確認並負責。